Skip to content

[member approval] configurable via environment#542

Closed
drew-u410 wants to merge 1 commit intosourcebot-dev:mainfrom
drew-u410:drew/member_approval_required_configuration
Closed

[member approval] configurable via environment#542
drew-u410 wants to merge 1 commit intosourcebot-dev:mainfrom
drew-u410:drew/member_approval_required_configuration

Conversation

@drew-u410
Copy link
Copy Markdown
Contributor

@drew-u410 drew-u410 commented Sep 26, 2025

  • Enable configuration of member approval via Env Var vs. a UI toggle.
  • Follows the same pattern as FORCE_ENABLE_ANONYMOUS_ACCESS.

Summary by CodeRabbit

  • New Features

    • Added environment variables to control anonymous access and require member approval.
    • Organization access settings now reflect enforced member approval by disabling the toggle and showing an explanatory message.
    • Single-tenant setups automatically apply the member approval requirement based on configuration.
  • Documentation

    • Documented the new member approval environment variable and its default.
  • Chores

    • Updated the changelog to note that member approval can be configured via an environment variable.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 26, 2025

Walkthrough

Introduces a new environment variable FORCE_MEMBER_APPROVAL_REQUIRED across config, docs, and UI. Propagates the flag through env schema and components, disables UI toggle when forced, documents it, and updates initialization to set/sync the organization’s memberApprovalRequired based on the env value with logging.

Changes

Cohort / File(s) Summary
Env and Documentation
/.env.development, /CHANGELOG.md, /docs/docs/configuration/environment-variables.mdx, /packages/web/src/env.mjs
Adds FORCE_MEMBER_APPROVAL_REQUIRED to sample env, changelog, docs table, and server env schema (optional boolean).
UI Propagation
/packages/web/src/app/components/memberApprovalRequiredToggle.tsx, /packages/web/src/app/components/organizationAccessSettingsWrapper.tsx, /packages/web/src/app/components/organizationAccessSettings.tsx
Adds optional prop forceMemberApprovalRequired, passes it through wrapper to toggle, disables toggle and shows message when defined, reads from env in settings and forwards.
Initialization Logic
/packages/web/src/initialize.ts
Reads FORCE_MEMBER_APPROVAL_REQUIRED during syncDeclarativeConfig and initSingleTenancy; updates org.memberApprovalRequired accordingly with logs, including repeated enforcement during init phases.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Env as Process Env
  participant App as App Init
  participant DB as Org Store
  participant Log as Logger

  rect rgba(230,245,255,0.5)
  note over App,DB: syncDeclarativeConfig
  App->>Env: Read FORCE_MEMBER_APPROVAL_REQUIRED
  alt Defined
    App->>DB: Fetch single-tenant org
    App->>DB: Update memberApprovalRequired
    App->>Log: Log applied env setting
  else Not defined
    App->>Log: Skip update
  end
  end

  rect rgba(235,255,235,0.5)
  note over App,DB: initSingleTenancy (creation)
  App->>Env: Read FORCE_MEMBER_APPROVAL_REQUIRED
  App->>DB: Create org with memberApprovalRequired (derived or default)
  App->>Log: Log creation with setting
  end

  rect rgba(255,245,230,0.5)
  note over App,DB: initSingleTenancy (post steps)
  App->>Env: Re-read FORCE_MEMBER_APPROVAL_REQUIRED
  alt Defined
    App->>DB: Ensure org memberApprovalRequired matches env
    App->>Log: Log enforced setting
  end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A toggle hops, then holds its ground,
Env winds whisper, “Force it down.”
The org now keeps its ears aligned,
Approval flagged, by env defined.
I nibble logs, contentedly—
Config carrots set by decree. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly summarizes the primary change by indicating that member approval behavior can now be configured through environment variables, which directly reflects the pull request’s objective of adding FORCE_MEMBER_APPROVAL_REQUIRED and related functionality.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@msukkari msukkari left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should force the UI for this if the env var is passed in as true, similar to what we do for FORCE_ENABLE_ANONYMOUS_ACCESS. Useful references:

const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");

The <code className="bg-[var(--secondary)] px-1 py-0.5 rounded text-xs font-mono">forceEnableAnonymousAccess</code> is set, so this cannot be changed from the UI.

Also changelog entry pls :)

| `DATABASE_URL` | `postgresql://postgres@ localhost:5432/sourcebot` | <p>Connection string of your Postgres database. By default, a Postgres database is automatically provisioned at startup within the container.</p><p>If you'd like to use a non-default schema, you can provide it as a parameter in the database url </p> |
| `EMAIL_FROM_ADDRESS` | `-` | <p>The email address that transactional emails will be sent from. See [this doc](/docs/configuration/transactional-emails) for more info.</p> |
| `FORCE_ENABLE_ANONYMOUS_ACCESS` | `false` | <p>When enabled, [anonymous access](/docs/configuration/auth/access-settings#anonymous-access) to the organization will always be enabled</p>
| `MEMBER_APPROVAL_REQUIRED` | `true` | <p>When enabled, new users will need approval from an organization owner before they can access your deployment. See [access settings docs](/docs/configuration/auth/access-settings) for more info</p>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could we rename this to FORCE_MEMBER_APPROVAL_REQUIRED to match the other env var?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing - done!

@drew-u410 drew-u410 force-pushed the drew/member_approval_required_configuration branch from baf2caa to 2335c78 Compare September 29, 2025 12:17
@drew-u410 drew-u410 force-pushed the drew/member_approval_required_configuration branch 2 times, most recently from 7fa9ba6 to e401810 Compare September 29, 2025 12:21
@drew-u410
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Sep 29, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
docs/docs/configuration/environment-variables.mdx (1)

25-25: Document scope (single‑tenant) and valid values

Add scope note and accepted values to reduce confusion.

-| `FORCE_MEMBER_APPROVAL_REQUIRED` | `-` | <p>When set to `true` or `false`, forces the member approval requirement setting and disables the UI toggle. When enabled, new users will need approval from an organization owner before they can access your deployment. See [access settings docs](/docs/configuration/auth/access-settings) for more info</p>
+| `FORCE_MEMBER_APPROVAL_REQUIRED` | `-` | <p>Single‑tenant mode only. When set to `true` or `false`, forces the member‑approval setting and disables the UI toggle (valid values: `true`/`false`). When `true`, new users need approval from an organization owner before they can access your deployment. See [access settings docs](/docs/configuration/auth/access-settings).</p>
.env.development (1)

26-27: Add brief hints for values and scope

Tiny comment tweaks improve DX.

-# FORCE_ENABLE_ANONYMOUS_ACCESS="false"
-# FORCE_MEMBER_APPROVAL_REQUIRED="false"
+# FORCE_ENABLE_ANONYMOUS_ACCESS="false"   # valid: "true"|"false"
+# FORCE_MEMBER_APPROVAL_REQUIRED="false"  # single-tenant only; disables UI toggle; valid: "true"|"false"
packages/web/src/initialize.ts (2)

196-199: Simplify defaulting logic on org create

Same behavior, clearer expression.

-                    memberApprovalRequired: env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' ? true : 
-                                          env.FORCE_MEMBER_APPROVAL_REQUIRED === 'false' ? false : 
-                                          true, // default to true if FORCE_MEMBER_APPROVAL_REQUIRED is not set
+                    // Default to true when unset; honor explicit true/false.
+                    memberApprovalRequired: env.FORCE_MEMBER_APPROVAL_REQUIRED === 'false' ? false : true,

239-250: DRY this block and avoid unnecessary writes

Extract into a helper and no‑op if the value is already correct to reduce logs/writes.

-    if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
-        const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';
-        const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
-        if (org) {
-            await prisma.org.update({
-                where: { id: org.id },
-                data: { memberApprovalRequired: forceMemberApprovalRequired },
-            });
-            logger.info(`Member approval required set to ${forceMemberApprovalRequired} via FORCE_MEMBER_APPROVAL_REQUIRED environment variable`);
-        }
-    }
+    await applyForcedMemberApprovalRequired();

Place this helper near the top of the file:

async function applyForcedMemberApprovalRequired() {
  if (env.FORCE_MEMBER_APPROVAL_REQUIRED === undefined) return;
  const desired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';
  const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
  if (!org || org.memberApprovalRequired === desired) return;
  await prisma.org.update({ where: { id: org.id }, data: { memberApprovalRequired: desired } });
  logger.info(`Member approval required set to ${desired} via FORCE_MEMBER_APPROVAL_REQUIRED`);
}

Also replace the identical block in syncDeclarativeConfig (Lines 134–145) with await applyForcedMemberApprovalRequired();.

packages/web/src/app/components/organizationAccessSettingsWrapper.tsx (1)

11-11: Normalize type to boolean for clarity and consistency with anonymous access flag.

Passing a raw string down the tree makes the UI treat any non-undefined value as “forced”. Prefer boolean|undefined, with parsing done in the server component, to avoid surprising behavior when set to an invalid string and to mirror forceEnableAnonymousAccess.

Apply in this file:

-    forceMemberApprovalRequired?: string
+    forceMemberApprovalRequired?: boolean

Optional (keeps the InviteLink panel in sync if memberApprovalRequired changes externally):

// add near useState
useEffect(() => setShowInviteLink(memberApprovalRequired), [memberApprovalRequired])

Also applies to: 17-19, 32-32

packages/web/src/app/components/memberApprovalRequiredToggle.tsx (2)

13-13: Use boolean|undefined for forceMemberApprovalRequired and keep the signature aligned.

This avoids treating arbitrary strings as “forced” and makes intent explicit.

-    forceMemberApprovalRequired?: string
+    forceMemberApprovalRequired?: boolean

Signature stays the same after the type change.

Also applies to: 16-16


49-51: Derive isForced, show enforced value, and add a11y on the icon.

  • Compute isForced from the boolean prop.
  • Message should indicate whether it’s enforced to true or false.
  • Mark decorative SVG as aria-hidden.
-    const isDisabled = isLoading || forceMemberApprovalRequired !== undefined;
-    const showForceMessage = forceMemberApprovalRequired !== undefined;
+    const isForced = forceMemberApprovalRequired !== undefined;
+    const isDisabled = isLoading || isForced;
+    const showForceMessage = isForced;
-                                    <svg 
+                                    <svg 
                                         className="w-4 h-4 mt-0.5 flex-shrink-0" 
                                         fill="none" 
                                         stroke="currentColor" 
                                         viewBox="0 0 24 24"
+                                        aria-hidden="true"
                                     >
-                                    <span>
-                                        The <code className="bg-[var(--secondary)] px-1 py-0.5 rounded text-xs font-mono">FORCE_MEMBER_APPROVAL_REQUIRED</code> environment variable is set, so this cannot be changed from the UI.
-                                    </span>
+                                    <span>
+                                        The <code className="bg-[var(--secondary)] px-1 py-0.5 rounded text-xs font-mono">FORCE_MEMBER_APPROVAL_REQUIRED</code> env var is set to <code className="bg-[var(--secondary)] px-1 py-0.5 rounded text-xs font-mono">{String(forceMemberApprovalRequired)}</code>. This is enforced and cannot be changed from the UI.
+                                    </span>

(Optional safety guard to prevent accidental calls even if a disabled control fires):

// early in handleToggle
if (forceMemberApprovalRequired !== undefined) return;

Also applies to: 63-84, 91-91

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa62847 and e401810.

📒 Files selected for processing (8)
  • .env.development (1 hunks)
  • CHANGELOG.md (1 hunks)
  • docs/docs/configuration/environment-variables.mdx (1 hunks)
  • packages/web/src/app/components/memberApprovalRequiredToggle.tsx (3 hunks)
  • packages/web/src/app/components/organizationAccessSettings.tsx (2 hunks)
  • packages/web/src/app/components/organizationAccessSettingsWrapper.tsx (2 hunks)
  • packages/web/src/env.mjs (1 hunks)
  • packages/web/src/initialize.ts (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: build
🔇 Additional comments (1)
packages/web/src/env.mjs (1)

24-25: LGTM: env schema entry aligns with usage

Optional z.enum(['true','false']) mirrors existing flags and supports the “unset” default.

const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");

const forceEnableAnonymousAccess = env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true';
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Normalize FORCE_MEMBER_APPROVAL_REQUIRED to boolean|undefined before passing to UI.

Keeps semantics clear (enforced true/false vs. not enforced) and aligns with forceEnableAnonymousAccess.

-    const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
+    const forceMemberApprovalRequired = (() => {
+        const v = env.FORCE_MEMBER_APPROVAL_REQUIRED?.toLowerCase();
+        if (v === "true") return true;
+        if (v === "false") return false;
+        return undefined;
+    })();

Run to confirm there are no other string-typed usages that would conflict:

Also applies to: 41-41


🏁 Script executed:

#!/bin/bash
# Verify all references and ensure downstream types can accept boolean|undefined
rg -n --pretty --no-ignore 'FORCE_MEMBER_APPROVAL_REQUIRED'
rg -n --type=ts -C2 'forceMemberApprovalRequired\??:\s*string'

Length of output: 3006


Normalize FORCE_MEMBER_APPROVAL_REQUIRED to boolean|undefined across UI components

  • In organizationAccessSettings.tsx, coerce the raw env string to boolean | undefined:
- const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
+ const forceMemberApprovalRequired = (() => {
+   const v = env.FORCE_MEMBER_APPROVAL_REQUIRED?.toLowerCase();
+   if (v === 'true') return true;
+   if (v === 'false') return false;
+   return undefined;
+ })();
  • Update prop types to match in organizationAccessSettingsWrapper.tsx and memberApprovalRequiredToggle.tsx:
-  forceMemberApprovalRequired?: string
+  forceMemberApprovalRequired?: boolean
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
// before: raw string from env
const forceMemberApprovalRequired = (() => {
const v = env.FORCE_MEMBER_APPROVAL_REQUIRED?.toLowerCase();
if (v === 'true') return true;
if (v === 'false') return false;
return undefined;
})();
🤖 Prompt for AI Agents
In packages/web/src/app/components/organizationAccessSettings.tsx around line
27, env.FORCE_MEMBER_APPROVAL_REQUIRED is currently a raw string; coerce it to
boolean | undefined (e.g., if value === "true" return true, if value === "false"
return false, otherwise undefined). Then update the prop types in
packages/web/src/app/components/organizationAccessSettingsWrapper.tsx and
packages/web/src/app/components/memberApprovalRequiredToggle.tsx to accept
boolean | undefined and ensure all places that pass this value forward use the
normalized boolean/undefined rather than the raw env string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coderabbit is probably right here although its suggestion is verbose. env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' should be fine

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@msukkari You're absolutely right! The simpler approach is much cleaner and follows the same pattern as forceEnableAnonymousAccess. Here's the updated suggestion:

const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';

This maintains consistency with the existing forceEnableAnonymousAccess pattern and is much more readable. We'll also need to update the prop types in the downstream components from string to boolean to match this change.


🏁 Script executed:

#!/bin/bash
# Check the prop types for forceMemberApprovalRequired in the downstream components
rg -n --type=ts -A5 -B5 'forceMemberApprovalRequired.*:.*string' packages/web/src/app/components/

Length of output: 2353


Perfect! Now I can see the complete picture. Here are the changes needed across all three components to implement the simpler boolean approach:

1. In packages/web/src/app/components/organizationAccessSettings.tsx:

const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';

2. In packages/web/src/app/components/organizationAccessSettingsWrapper.tsx:

interface OrganizationAccessSettingsWrapperProps {
    memberApprovalRequired: boolean
    inviteLinkEnabled: boolean
    inviteLink: string | null
    forceMemberApprovalRequired?: boolean  // Changed from string to boolean
}

3. In packages/web/src/app/components/memberApprovalRequiredToggle.tsx:

interface MemberApprovalRequiredToggleProps {
    memberApprovalRequired: boolean
    onToggleChange?: (checked: boolean) => void
    forceMemberApprovalRequired?: boolean  // Changed from string to boolean
}

This approach is much cleaner, follows the established pattern with forceEnableAnonymousAccess, and maintains type safety throughout the component chain. The boolean value will work perfectly with the UI logic that checks forceMemberApprovalRequired !== undefined to determine if the toggle should be disabled.

Comment on lines +134 to +145
// Apply FORCE_MEMBER_APPROVAL_REQUIRED environment variable setting
if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
if (org) {
await prisma.org.update({
where: { id: org.id },
data: { memberApprovalRequired: forceMemberApprovalRequired },
});
logger.info(`Member approval required set to ${forceMemberApprovalRequired} via FORCE_MEMBER_APPROVAL_REQUIRED environment variable`);
}
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Force must be enforced server‑side, not only at init

Currently this sets the value on startup/sync, but an API call could still flip memberApprovalRequired if the UI is bypassed. Add a guard in the write path (e.g., access‑settings update handler) to reject changes when FORCE_MEMBER_APPROVAL_REQUIRED is defined.

Run to locate and patch the update path(s):

Suggested server check (illustrative):

// In the handler that updates org access settings:
import { env } from '@/env.mjs';

if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
  return new Response(JSON.stringify({ error: 'memberApprovalRequired is forced by env' }), { status: 409 });
}

🏁 Script executed:

#!/bin/bash
# Find all updates to memberApprovalRequired and the access settings API.
rg -nP -C3 '\bmemberApprovalRequired\s*:' --type=ts
rg -nP -C3 '(access|org).*settings|memberApproval' --type=ts -g '!**/node_modules/**'

Length of output: 4482


Enforce FORCE_MEMBER_APPROVAL_REQUIRED in org update endpoint (actions.ts:1816–1822)

  • Add a guard around the prisma.org.update call in packages/web/src/actions.ts (lines 1816–1822) to return a 409 error if env.FORCE_MEMBER_APPROVAL_REQUIRED is set.
  • Extract the override logic into a shared helper and skip the database update when the existing value already matches the enforced flag.
🤖 Prompt for AI Agents
In packages/web/src/initialize.ts around lines 134–145, the code unconditionally
updates org.memberApprovalRequired when FORCE_MEMBER_APPROVAL_REQUIRED is set;
extract this override logic into a shared helper (e.g., services/orgOverride.ts)
that accepts the enforced boolean and the org id and only performs a
prisma.org.update when the current DB value differs, and update initialize.ts to
call that helper instead of doing the update inline; then in
packages/web/src/actions.ts around lines 1816–1822, add a guard that checks
process.env.FORCE_MEMBER_APPROVAL_REQUIRED and if present returns a 409 response
for any request attempting to change memberApprovalRequired (using the same
shared helper to determine the enforced value), thereby preventing DB changes
and avoiding redundant updates when the current value already matches the
enforced flag.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not super needed since the only way we set this is in the UI and we're properly disabling it when the env var is enabeld

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@msukkari I understand your point about the UI being disabled, but I'd like to verify the API surface to ensure there aren't other ways to bypass this protection.


🏁 Script executed:

#!/bin/bash
# Find API routes and actions that can modify memberApprovalRequired
rg -nP -C5 'memberApprovalRequired.*=|memberApprovalRequired.*:' --type=ts -g '!**/node_modules/**'

# Look for API endpoints or server actions related to org settings
rg -nP -C3 'export.*action|export.*POST|export.*PUT' --type=ts -g '**/actions.ts' -g '**/api/**'

# Check for any direct prisma updates to org.memberApprovalRequired
rg -nP -C3 'prisma\.org\.update.*memberApprovalRequired|memberApprovalRequired.*prisma\.org\.update' --type=ts

Length of output: 3596


🏁 Script executed:

#!/bin/bash
# Find the actions.ts file and any org update functionality
fd -t f "actions.ts" --exec rg -nP -C5 'memberApprovalRequired' {} \;

# Look for org update server actions more broadly
rg -nP -C3 'org.*update|update.*org' --type=ts -g '!**/node_modules/**'

# Search for any prisma org updates
rg -nP -C3 'prisma\.org\.update' --type=ts -g '!**/node_modules/**'

Length of output: 1422


@msukkari I found the server action that can bypass the UI protection. Looking at the code, there's a setMemberApprovalRequired function that directly updates the database without checking the environment variable:

// From actions.ts lines 1814-1824
export const setMemberApprovalRequired = async (domain: string, required: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () =>
    withAuth(async (userId) =>
        withOrgMembership(userId, domain, async ({ org }) => {
            await prisma.org.update({
                where: { id: org.id },
                data: { memberApprovalRequired: required },
            });

            return {
                success: true,
            };

This server action can be called directly via form actions or fetch requests, completely bypassing the disabled UI toggle. While the UI protection is good UX, relying solely on client-side controls for enforcement isn't secure.

A simple server-side check in this function would ensure the environment variable setting is truly enforced:

if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
    return { statusCode: 409, errorCode: "SETTING_FORCED_BY_ENV", message: "Member approval setting is controlled by environment variable" };
}

This would provide defense in depth and ensure the configuration can't be circumvented.

}

const isDisabled = isLoading || forceMemberApprovalRequired !== undefined;
const showForceMessage = forceMemberApprovalRequired !== undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to the forceEnableAnonymousAccess flag, we probably only want to show this message if forceMemberApprovalRequired is true

Comment on lines +196 to +198
memberApprovalRequired: env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' ? true :
env.FORCE_MEMBER_APPROVAL_REQUIRED === 'false' ? false :
true, // default to true if FORCE_MEMBER_APPROVAL_REQUIRED is not set
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
memberApprovalRequired: env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' ? true :
env.FORCE_MEMBER_APPROVAL_REQUIRED === 'false' ? false :
true, // default to true if FORCE_MEMBER_APPROVAL_REQUIRED is not set
...(env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' && {
memberApprovalRequired: true,
})

default values for data in the db are set by the schema so its best to not have them set in the code. We only want to set this if the env var is true anyways

}
}

const isDisabled = isLoading || forceMemberApprovalRequired !== undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to forceEnableAnonymousAccess we only need to disable if the forceMemberApprovalRequired flag is true


// Auth
FORCE_ENABLE_ANONYMOUS_ACCESS: booleanSchema.default('false'),
FORCE_MEMBER_APPROVAL_REQUIRED: booleanSchema.optional(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

following the rest of the changes to only force UI stuff when this is enabled, we can also default this to false to match the other env var

@drew-u410
Copy link
Copy Markdown
Contributor Author

@msukkari, I think it is best for you to take this over or for me to close. I don't have the context you do and I think you are requesting changes to nearly every line. :)

@drew-u410 drew-u410 closed this Oct 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants